import path from 'path'
import os from 'os'

import {
  HomeServerManagerInterface,
  HomeServerEnvironmentConfiguration,
} from '@web/Application/Device/DesktopSnjsExports'
import { HomeServer, HomeServerInterface } from '@standardnotes/home-server'

import { WebContents } from 'electron'
import { MessageToWebApp } from '../../Shared/IpcMessages'
import { FilesManagerInterface } from '../File/FilesManagerInterface'
import { HomeServerConfigurationFile } from './HomeServerConfigurationFile'

export class HomeServerManager implements HomeServerManagerInterface {
  private readonly HOME_SERVER_CONFIGURATION_FILE_NAME = 'config.json'

  private homeServerConfiguration: HomeServerEnvironmentConfiguration | undefined
  private homeServerDataLocation: string | undefined
  private lastErrorMessage: string | undefined
  private logs: string[] = []

  private readonly LOGS_BUFFER_SIZE = 1000

  private homeServer?: HomeServerInterface

  constructor(
    private webContents: WebContents,
    private filesManager: FilesManagerInterface,
  ) {}

  async getHomeServerUrl(): Promise<string | undefined> {
    const homeServerConfiguration = await this.getHomeServerConfigurationObject()
    if (!homeServerConfiguration) {
      return undefined
    }

    return `http://${this.getLocalIP()}:${homeServerConfiguration.port}`
  }

  async isHomeServerRunning(): Promise<boolean> {
    return this.homeServer?.isRunning() ?? false
  }

  async getHomeServerLastErrorMessage(): Promise<string | undefined> {
    return this.lastErrorMessage
  }

  async activatePremiumFeatures(username: string, subscriptionId: number): Promise<string | undefined> {
    if (!this.homeServer) {
      return
    }

    const result = await this.homeServer.activatePremiumFeatures({
      username,
      subscriptionId,
    })

    if (result.isFailed()) {
      return result.getError()
    }
  }

  async getHomeServerConfiguration(): Promise<string | undefined> {
    if (this.homeServerConfiguration) {
      return JSON.stringify(this.homeServerConfiguration)
    }

    if (!this.homeServerDataLocation) {
      return undefined
    }

    const homeServerConfiguration = await this.filesManager.readJSONFile<HomeServerConfigurationFile>(
      path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME),
    )
    if (!homeServerConfiguration) {
      return undefined
    }

    return JSON.stringify(homeServerConfiguration.configuration)
  }

  async setHomeServerConfiguration(configurationJSONString: string): Promise<void> {
    try {
      if (!this.homeServerDataLocation) {
        throw new Error('Home server data location is not set.')
      }

      const homeServerConfiguration = JSON.parse(configurationJSONString) as HomeServerEnvironmentConfiguration

      await this.filesManager.ensureDirectoryExists(this.homeServerDataLocation)

      const configurationFile: HomeServerConfigurationFile = {
        version: '1.0.0',
        info: {
          warning: 'Do not edit this file.',
          information:
            'The values below are encrypted with a key created by the desktop application after installation. The key is stored in your secure device keychain.',
          instructions:
            'Put this file inside your home server data location to restore your home server configuration.',
        },
        configuration: homeServerConfiguration,
      }

      await this.filesManager.writeJSONFile(
        path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME),
        configurationFile,
      )

      this.homeServerConfiguration = homeServerConfiguration
    } catch (error) {
      console.error(`Could not save server configuration: ${(error as Error).message}`)
    }
  }

  async setHomeServerDataLocation(location: string): Promise<void> {
    this.homeServerDataLocation = location
  }

  async stopHomeServer(): Promise<string | undefined> {
    if (!this.homeServer) {
      return
    }

    const result = await this.homeServer.stop()
    if (result.isFailed()) {
      return result.getError()
    }

    return undefined
  }

  async startHomeServer(): Promise<string | undefined> {
    this.homeServer = new HomeServer()

    try {
      this.lastErrorMessage = undefined
      this.logs = []

      let homeServerConfiguration = await this.getHomeServerConfigurationObject()
      if (!homeServerConfiguration) {
        homeServerConfiguration = this.generateHomeServerConfiguration()
      }
      await this.setHomeServerConfiguration(JSON.stringify(homeServerConfiguration))

      if (!this.homeServerDataLocation) {
        this.lastErrorMessage = 'Home server data location is not set.'

        return this.lastErrorMessage
      }

      const {
        jwtSecret,
        authJwtSecret,
        encryptionServerKey,
        pseudoKeyParamsKey,
        valetTokenSecret,
        port,
        logLevel,
        databaseEngine,
        mysqlConfiguration,
      } = homeServerConfiguration as HomeServerEnvironmentConfiguration

      const environment: { [name: string]: string } = {
        JWT_SECRET: jwtSecret,
        AUTH_JWT_SECRET: authJwtSecret,
        ENCRYPTION_SERVER_KEY: encryptionServerKey,
        PSEUDO_KEY_PARAMS_KEY: pseudoKeyParamsKey,
        VALET_TOKEN_SECRET: valetTokenSecret,
        FILES_SERVER_URL: (await this.getHomeServerUrl()) as string,
        LOG_LEVEL: logLevel ?? 'info',
        VERSION: 'desktop',
        PORT: port.toString(),
        DB_TYPE: databaseEngine,
      }

      if (mysqlConfiguration !== undefined) {
        environment.DB_HOST = mysqlConfiguration.host
        if (mysqlConfiguration.port) {
          environment.DB_PORT = mysqlConfiguration.port.toString()
        }
        environment.DB_USERNAME = mysqlConfiguration.username
        environment.DB_PASSWORD = mysqlConfiguration.password
        environment.DB_DATABASE = mysqlConfiguration.database
      }

      const result = await this.homeServer.start({
        dataDirectoryPath: this.homeServerDataLocation,
        environment,
        logStreamCallback: this.appendLogs.bind(this),
      })

      if (result.isFailed()) {
        this.lastErrorMessage = result.getError()

        return this.lastErrorMessage
      }

      this.webContents.send(MessageToWebApp.HomeServerStarted, await this.getHomeServerUrl())
    } catch (error) {
      return (error as Error).message
    }
  }

  async getHomeServerLogs(): Promise<string[]> {
    return this.logs
  }

  private appendLogs(log: Buffer): void {
    this.logs.push(log.toString())

    if (this.logs.length > this.LOGS_BUFFER_SIZE) {
      this.logs.shift()
    }
  }

  private generateRandomKey(length: number): string {
    return require('crypto').randomBytes(length).toString('hex')
  }

  private getLocalIP() {
    const interfaces = os.networkInterfaces()
    let internalAddress = undefined
    for (const interfaceName in interfaces) {
      const addresses = interfaces[interfaceName]
      if (!addresses) {
        continue
      }

      for (const address of addresses) {
        if (address.family === 'IPv4') {
          if (!address.internal) {
            return address.address
          }

          internalAddress = address.address
        }
      }
    }

    return internalAddress
  }

  private async getHomeServerConfigurationObject(): Promise<HomeServerEnvironmentConfiguration | undefined> {
    try {
      const homeServerConfigurationJSON = await this.getHomeServerConfiguration()
      if (!homeServerConfigurationJSON) {
        return undefined
      }

      return JSON.parse(homeServerConfigurationJSON)
    } catch (error) {
      console.error(`Could not get home server configuration: ${(error as Error).message}`)
    }
  }

  private generateHomeServerConfiguration(): HomeServerEnvironmentConfiguration {
    const jwtSecret = this.generateRandomKey(32)
    const authJwtSecret = this.generateRandomKey(32)
    const encryptionServerKey = this.generateRandomKey(32)
    const pseudoKeyParamsKey = this.generateRandomKey(32)
    const valetTokenSecret = this.generateRandomKey(32)
    const port = 3127

    const configuration: HomeServerEnvironmentConfiguration = {
      jwtSecret,
      authJwtSecret,
      encryptionServerKey,
      pseudoKeyParamsKey,
      valetTokenSecret,
      port,
      databaseEngine: 'sqlite',
      logLevel: 'info',
    }

    return configuration
  }
}
